/*
 * Copyright 2012 Gayan Perera
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package gap.maven.script.components;

import edu.emory.mathcs.backport.java.util.Collections;
import gap.maven.script.components.api.ArtifactFile;
import gap.maven.script.components.api.ExtractorPattern;
import gap.maven.script.components.api.POMArtifactFile;
import gap.maven.script.components.spi.FileExtractor;
import gap.maven.script.helpers.AbstractLinker;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.regex.Pattern;

import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.factory.ArtifactFactory;
import org.apache.maven.artifact.repository.ArtifactRepository;
import org.apache.maven.artifact.resolver.ArtifactNotFoundException;
import org.apache.maven.artifact.resolver.ArtifactResolutionException;
import org.apache.maven.artifact.resolver.ArtifactResolver;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.model.Dependency;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.PluginManager;
import org.apache.maven.plugin.logging.Log;
import org.apache.maven.project.MavenProject;
import org.apache.maven.project.MavenProjectBuilder;
import org.apache.maven.project.ProjectBuildingException;
import org.codehaus.plexus.util.FileUtils;
import org.twdata.maven.mojoexecutor.MojoExecutor;
import org.twdata.maven.mojoexecutor.MojoExecutor.Element;

/**
 * This class implements the {@link ArtifactFile} and {@link POMArtifactFile}.
 * 
 * @author gayan.perera
 *
 */
final class MavenArtifactFile implements ArtifactFile, POMArtifactFile {
    private final Log log;
    private final ArtifactFactory artifactFactory;
    private final ArtifactResolver resolver;
    private final ArtifactRepository localRepository;
    @SuppressWarnings("rawtypes")
    private final List remoteRepositories;
    private final MavenProjectBuilder projectBuilder;
    private final ComponentContext context;

    private Artifact artifact = null;
    private MavenProject project;

    private final static String ERROR_MSG_NOT_RESOLVED = "Current artifact instance is not yet resolved.";

    MavenArtifactFile(String groupId, String artifactId, String version, String type, String classifier,
            ComponentContext context) {
        this.context = context;
        this.log = context.lookup(ComponentContext.COMPONENT_KEY_LOG);
        this.artifactFactory = context.lookup(ComponentContext.COMPONENT_KEY_ARTIFACT_FACTORY);
        this.resolver = context.lookup(ComponentContext.COMPONENT_KEY_ARTIFACT_RESOLVER);
        this.projectBuilder = context.lookup(ComponentContext.COMPONENT_KEY_PROJECT_BUILDER);
        this.localRepository = context.lookup(ComponentContext.COMPONENT_KEY_LOCAL_REPOSITORY);
        this.remoteRepositories = context.lookup(ComponentContext.COMPONENT_KEY_REMOTE_REPOSITORY_LIST);

        // creating the artifact and the project from it.
        if (type.length() == 0) {
            type = "jar";
        }

        if ((classifier == null) || (classifier.length() == 0)) {
            artifact = artifactFactory.createArtifact(groupId, artifactId, version, "", type);
        } else {
            artifact = artifactFactory.createArtifactWithClassifier(groupId, artifactId, version, type, classifier);
        }

        // try to build the project if fail warn and continue since we will try to load it later.
        try {
            this.project = projectBuilder.buildFromRepository(
                    artifactFactory.createArtifact(groupId, artifactId, version, "", "pom"), remoteRepositories,
                    localRepository);
        } catch (ProjectBuildingException ex) {
            throw new RuntimeException("Failed to build the maven project.", ex);
        }
    }

    MavenArtifactFile(MavenProject project, ComponentContext context) {
        this.project = project;
        this.context = context;
        this.log = context.lookup(ComponentContext.COMPONENT_KEY_LOG);
        this.artifactFactory = context.lookup(ComponentContext.COMPONENT_KEY_ARTIFACT_FACTORY);
        this.resolver = context.lookup(ComponentContext.COMPONENT_KEY_ARTIFACT_RESOLVER);
        this.projectBuilder = context.lookup(ComponentContext.COMPONENT_KEY_PROJECT_BUILDER);
        this.localRepository = context.lookup(ComponentContext.COMPONENT_KEY_LOCAL_REPOSITORY);
        this.remoteRepositories = context.lookup(ComponentContext.COMPONENT_KEY_REMOTE_REPOSITORY_LIST);

        artifact = artifactFactory.createArtifact(project.getGroupId(), project.getArtifactId(), project.getVersion(),
                "", "pom");
    }

    MavenArtifactFile(Artifact artifact, ComponentContext context) {
        this(artifact.getGroupId(), artifact.getArtifactId(), artifact.getVersion(), artifact.getType(), artifact
                .getClassifier(), context);
    }

    MavenArtifactFile(Dependency dependency, ComponentContext context) {
        this(dependency.getGroupId(), dependency.getArtifactId(), dependency.getVersion(), "pom", dependency
                .getClassifier(), context);
    }

    public boolean link(String linkPath) {
        final AbstractLinker linker = context.lookup(ComponentContext.COMPONENT_KEY_LINKER);
        try {
            linker.createFileSymbolicLink(artifact.getFile().getAbsolutePath(), linkPath);
            return true;
        } catch (IOException ex) {
            log.error(ex);
        }
        return false;
    }

    public File copy(String destDir) {
        final File dir = new File(destDir);
        final File artifactFile = artifact.getFile();

        if ((artifactFile != null) && dir.isDirectory()) {
            try {
                FileUtils.copyFileToDirectory(artifactFile, new File(destDir));
                return new File(destDir, artifactFile.getName());
            } catch (IOException ex) {
                log.error(ex);
            }
        }
        return null;
    }

    public boolean download() {
        // Don't try to resolve if its already resolved.
        if (artifact.isResolved()) {
            return true;
        }

        try {
            resolver.resolve(artifact, remoteRepositories, localRepository);
            return artifact.isResolved();
        } catch (ArtifactResolutionException ex) {
            log.error(ex);
        } catch (ArtifactNotFoundException ex) {
            log.error(ex);
        }
        return false;
    }

    public ArtifactFile getClassifier(String classifier) {
        return getClassifier("", classifier);
    }

    public ArtifactFile getClassifier(String type, String classifier) {
        return new MavenArtifactFile(artifact.getGroupId(), artifact.getArtifactId(), artifact.getVersion(), type,
                classifier, context);
    }

    public ArtifactFile getMainArtifact(String type) {
        validateArtifactState();
        return new MavenArtifactFile(artifact.getGroupId(), artifact.getArtifactId(), artifact.getVersion(), type, "",
                context);
    }

    public ArtifactFile getMainArtifact() {
        if (isResolved() && !artifact.getType().equals("pom") && (artifact.getClassifier().length() == 0)) {
            return this;
        }
        return getMainArtifact("");
    }

    public File getFile() {
        return (artifact != null) ? artifact.getFile() : null;
    }

    protected boolean isResolved() {
        return (artifact != null) && (artifact.isResolved());
    }

    public boolean hasDependencies() {
        if (isResolved()) {
            @SuppressWarnings("unchecked")
            final List<Dependency> dependencies = project.getDependencies();
            return (dependencies != null) && !dependencies.isEmpty();
        }
        return false;
    }

    @SuppressWarnings("unchecked")
    public List<POMArtifactFile> getDependencyPOMs() {
        if (isResolved()) {
            return toPOMArtifacts(project.getDependencies());
        }
        throw new RuntimeException(ERROR_MSG_NOT_RESOLVED);
    }

    private List<POMArtifactFile> toPOMArtifacts(List<Dependency> dependencies) {
        final HashMap<String, Dependency> distinctDependencies = new HashMap<String, Dependency>();
        for (Dependency dependency : dependencies) {
            distinctDependencies.put(
                    dependency.getGroupId() + ":" + dependency.getArtifactId() + ":" + dependency.getVersion(),
                    dependency);
        }

        ArrayList<POMArtifactFile> pomList = new ArrayList<POMArtifactFile>(distinctDependencies.size());
        for (Dependency dependency : distinctDependencies.values()) {
            pomList.add(new MavenArtifactFile(dependency.getGroupId(), dependency.getArtifactId(), dependency
                    .getVersion(), "pom", "", context));
        }

        return pomList;
    }

    public String findVersionInDependencies(String groupId, String artifactId) throws MojoExecutionException {
        return findVersionInDependencies(groupId, artifactId, "", "");
    }

    public String findVersionInDependencies(String groupId, String artifactId, String type, String classifier)
            throws MojoExecutionException {
        if (groupId == null || groupId.length() == 0) {
            throw new IllegalArgumentException("groupId cannot be null or empty.");
        }

        if (artifactId == null || artifactId.length() == 0) {
            throw new IllegalArgumentException("artifactId cannot be null or empty.");
        }

        if (isResolved()) {
            @SuppressWarnings("unchecked")
            final List<Dependency> dependencies = project.getDependencies();
            String result = findVersionInDependencyList(dependencies, groupId, artifactId, type, classifier);

            if (result != null) {
                return result;
            } else {

                throw new MojoExecutionException("version cannot be found in dependencies.");
            }
        }
        throw new RuntimeException(ERROR_MSG_NOT_RESOLVED);
    }

    public String findVersionInManagedDependencies(String groupId, String artifactId) throws MojoExecutionException {
        return findVersionInManagedDependencies(groupId, artifactId, "", "");
    }

    public String findVersionInManagedDependencies(String groupId, String artifactId, String type, String classifier)
            throws MojoExecutionException {
        if (groupId == null || groupId.length() == 0) {
            throw new IllegalArgumentException("groupId cannot be null or empty.");
        }

        if (artifactId == null || artifactId.length() == 0) {
            throw new IllegalArgumentException("artifactId cannot be null or empty.");
        }

        if (isResolved()) {
            String result = findVersionInDependencyList(project.getDependencyManagement().getDependencies(), groupId,
                    artifactId, type, classifier);
            if (result != null) {
                return result;
            } else {

                throw new MojoExecutionException("version cannot be found in managed dependencies.");
            }
        }
        throw new RuntimeException(ERROR_MSG_NOT_RESOLVED);
    }

    private String findVersionInDependencyList(List<Dependency> dependencies, String groupIdRegex,
            String artifactIdRegex, String classifier, String type) throws MojoExecutionException {

        final Pattern groupIdPattern = Pattern.compile(groupIdRegex);
        final Pattern artifactIdPattern = Pattern.compile(artifactIdRegex);

        for (Dependency dependency : dependencies) {
            // mandatory
            boolean groupIdMatched = groupIdPattern.matcher(dependency.getGroupId()).matches();
            boolean artifactIdMatched = artifactIdPattern.matcher(dependency.getArtifactId()).matches();

            // optional
            boolean typeMatched = (type == null || type.length() == 0) ? true : dependency.getType().equals(type);
            boolean classifierMatched = (classifier == null || classifier.length() == 0) ? true : dependency
                    .getClassifier().equals(classifier);

            if (groupIdMatched && artifactIdMatched && typeMatched && classifierMatched) {
                return dependency.getVersion();
            }
        }
        return null;
    }

    public boolean hasManagedDependencies() {
        if (isResolved()) {
            List<Dependency> dependencies = project.getDependencyManagement().getDependencies();
            return (dependencies != null) && !dependencies.isEmpty();
        }
        throw new RuntimeException(ERROR_MSG_NOT_RESOLVED);
    }

    public List<POMArtifactFile> getManagedDependencyPOMs() {
        if (isResolved()) {
            return toPOMArtifacts(project.getDependencyManagement().getDependencies());
        }
        throw new RuntimeException(ERROR_MSG_NOT_RESOLVED);
    }

    public POMArtifactFile getPOM() {
        return new MavenArtifactFile(project, context);
    }

    public String getArtifactId() {
        validateArtifactState();
        return artifact.getArtifactId();
    }

    public String getVersion() {
        validateArtifactState();
        return artifact.getVersion();
    }

    public String getClassifier() {
        validateArtifactState();
        return artifact.getClassifier();
    }

    public String getType() {
        validateArtifactState();
        return artifact.getType();
    }

    public String getGroupId() {
        validateArtifactState();
        return artifact.getGroupId();
    }

    public String getPackaging() {
        return project.getPackaging();
    }

    public boolean extract(File toDir, ExtractorPattern... patterns) {
        if (!toDir.isDirectory()) {
            log.error("Argument toDir is not a directory.");
            return false;
        }

        final FileExtractor extractor = context.lookup(ComponentContext.COMPONENT_KEY_FILE_EXTRACTOR);
        try {
            if (patterns.length == 0) {
                if (log.isDebugEnabled()) {
                    log.debug("extracting all files in archive : " + getFile().getAbsolutePath());
                }
                extractor.extract(getFile(), toDir);
            } else {
                if (log.isDebugEnabled()) {
                    log.debug("extracting selected files by patterns in archive : " + getFile().getAbsolutePath());
                }
                extractor.extract(getFile(), toDir, patterns);
            }

            return true;
        } catch (IOException ex) {
            log.error(ex);
            return false;
        }
    }

    public List<ArtifactFile> getDependecyArtifactFiles() {
        validateArtifactState();
        // resolve the dependencies before reading the artifacts.
        try {
            MojoExecutor.executeMojo(
                    MojoExecutor.plugin("org.apache.maven.plugins", "maven-dependency-plugin"),
                    "resolve",
                    MojoExecutor.configuration(new Element[]{includeScope("runtime")}),
                    MojoExecutor.executionEnvironment(project,
                            context.<MavenSession> lookup(ComponentContext.COMPONENT_KEY_MAVEN_SESSION),
                            context.<PluginManager> lookup(ComponentContext.COMPONENT_KEY_PLUGIN_MANAGER)));
        } catch (MojoExecutionException ex) {
            log.error("Failed to resolve the dependencies.", ex);
            Collections.emptyList();
        }

        @SuppressWarnings("unchecked")
        final List<Artifact> artifacts = project.getRuntimeArtifacts();
        final ArrayList<ArtifactFile> artifactFiles = new ArrayList<ArtifactFile>(artifacts.size());

        for (Artifact artifact : artifacts) {
            artifactFiles.add(new MavenArtifactFile(artifact, context));
        }

        return artifactFiles;
    }

    @SuppressWarnings("unchecked")
    @Override
    public List<Dependency> getDependencies() {
        return project.getDependencies();
    }

    @Override
    public List<Dependency> getManagedDependencies() {
        return project.getDependencyManagement().getDependencies();
    }
    
    private void validateArtifactState() {
        if (artifact == null) {
            throw new IllegalStateException("Artifact is not downloaded. Call download() first.");
        }
    }

    @Override
    public String toString() {
        if (artifact == null) {
            return "Not resolved";
        }
        return new StringBuilder(artifact.getGroupId()).append(":").append(artifact.getArtifactId()).append(":")
                .append(artifact.getVersion()).append(":").append(artifact.getClassifier()).append(":")
                .append(artifact.getType()).toString();
    }
    
    private Element includeScope(String scope)
    {
        return new Element("includeScope", scope, new Element[0]);
    }
}
